<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Scatter &amp; Bubble</title>
		<meta name="viewport" content="width=device-width, initial-scale=1">

		<link rel="stylesheet" href="../dist/uPlot.min.css">
	</head>
	<body>
		<script src="./lib/quadtree.js"></script>

		<script type="module">
			import uPlot from "../dist/uPlot.esm.js";

			function randInt(min, max) {
				return Math.floor(Math.random() * (max - min + 1)) + min;
			}

			function filledArr(len, val) {
				let arr = Array(len);

				if (typeof val == "function") {
					for (let i = 0; i < len; ++i)
						arr[i] = val(i);
				}
				else {
					for (let i = 0; i < len; ++i)
						arr[i] = val;
				}

				return arr;
			}

			let points = 10000;
			let series = 5;

			console.time("prep");

			let data = filledArr(series, v => [
				filledArr(points, i => randInt(0,500)),
				filledArr(points, i => randInt(0,500)),
			]);

			data[0] = null;

			console.timeEnd("prep");

			console.log(data);

			const drawPoints = (u, seriesIdx, idx0, idx1) => {
				const size = 5 * pxRatio;

				uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
					let d = u.data[seriesIdx];

					u.ctx.fillStyle = series.stroke();

					let deg360 = 2 * Math.PI;

					console.time("points");

				//	let cir = new Path2D();
				//	cir.moveTo(0, 0);
				//	arc(cir, 0, 0, 3, 0, deg360);

					// Create transformation matrix that moves 200 points to the right
				//	let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
				//	m.a = 1;   m.b = 0;
				//	m.c = 0;   m.d = 1;
				//	m.e = 200; m.f = 0;


					let p = new Path2D();

					for (let i = 0; i < d[0].length; i++) {
						let xVal = d[0][i];
						let yVal = d[1][i];

						if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
							let cx = valToPosX(xVal, scaleX, xDim, xOff);
							let cy = valToPosY(yVal, scaleY, yDim, yOff);

							p.moveTo(cx + size/2, cy);
						//	arc(p, cx, cy, 3, 0, deg360);
							arc(p, cx, cy, size/2, 0, deg360);

						//	m.e = cx;
						//	m.f = cy;
						//	p.addPath(cir, m);

						//	qt.add({x: cx - 1.5, y: cy - 1.5, w: 3, h: 3, sidx: seriesIdx, didx: i});
						}
					}

					console.timeEnd("points");

					u.ctx.fill(p);
				});

				return null;
			};

			const drawPoints2 = (u, seriesIdx, idx0, idx1) => {
				uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
					let d = u.data[seriesIdx];

					u.ctx.fillStyle = series.fill();

					let deg360 = 2 * Math.PI;

					console.time("points");

				//	let cir = new Path2D();
				//	cir.moveTo(0, 0);
				//	arc(cir, 0, 0, 3, 0, deg360);

					// Create transformation matrix that moves 200 points to the right
				//	let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
				//	m.a = 1;   m.b = 0;
				//	m.c = 0;   m.d = 1;
				//	m.e = 200; m.f = 0;


					let p = new Path2D();

					let strokeWidth = 1;

					for (let i = 0; i < d[0].length; i++) {
						let cx = valToPosX(d[0][i], scaleX, xDim, xOff);
						let cy = valToPosY(d[1][i], scaleY, yDim, yOff);

						let size = d[2][i] * pxRatio;

						p.moveTo(cx + size, cy);
						arc(p, cx, cy, size, 0, deg360);
						qt.add({
							x: cx - size - strokeWidth/2 - u.bbox.left,
							y: cy - size - strokeWidth/2 - u.bbox.top,
							w: size * 2 + strokeWidth,
							h: size * 2 + strokeWidth,
							sidx: seriesIdx,
							didx: i
						});
					}

					console.timeEnd("points");

					u.ctx.fill(p);

					u.ctx.lineWidth = strokeWidth;
					u.ctx.strokeStyle = series.stroke();
					u.ctx.stroke(p);
				});

				return null;
			};

			// shape?
			const makeDrawPoints3 = (opts) => {
				let {/*size,*/ disp, each = () => {}} = opts;

				return (u, seriesIdx, idx0, idx1) => {
					uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
						let d = u.data[seriesIdx];

						let strokeWidth = 1;

						u.ctx.save();

						u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
						u.ctx.clip();

						u.ctx.fillStyle = series.fill();
						u.ctx.strokeStyle = series.stroke();
						u.ctx.lineWidth = strokeWidth;

						let deg360 = 2 * Math.PI;

						console.time("points");

					//	let cir = new Path2D();
					//	cir.moveTo(0, 0);
					//	arc(cir, 0, 0, 3, 0, deg360);

						// Create transformation matrix that moves 200 points to the right
					//	let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
					//	m.a = 1;   m.b = 0;
					//	m.c = 0;   m.d = 1;
					//	m.e = 200; m.f = 0;

						// compute bubble dims
						let sizes = disp.size.values(u, seriesIdx, idx0, idx1);

						// todo: this depends on direction & orientation
						// todo: calc once per redraw, not per path
						let filtLft = u.posToVal(-maxSize / 2, scaleX.key);
						let filtRgt = u.posToVal(u.bbox.width / pxRatio + maxSize / 2, scaleX.key);
						let filtBtm = u.posToVal(u.bbox.height / pxRatio + maxSize / 2, scaleY.key);
						let filtTop = u.posToVal(-maxSize / 2, scaleY.key);

						for (let i = 0; i < d[0].length; i++) {
							let xVal = d[0][i];
							let yVal = d[1][i];
							let size = sizes[i] * pxRatio;

							if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
								let cx = valToPosX(xVal, scaleX, xDim, xOff);
								let cy = valToPosY(yVal, scaleY, yDim, yOff);

								u.ctx.moveTo(cx + size/2, cy);
								u.ctx.beginPath();
								u.ctx.arc(cx, cy, size/2, 0, deg360);
								u.ctx.fill();
								u.ctx.stroke();

								each(u, seriesIdx, i,
									cx - size/2 - strokeWidth/2,
									cy - size/2 - strokeWidth/2,
									size + strokeWidth,
									size + strokeWidth
								);
							}
						}

						console.timeEnd("points");

						u.ctx.restore();
					});

					return null;
				};
			};

			// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
			let qt;

			let pxRatio;

			function setPxRatio() {
				pxRatio = uPlot.pxRatio;
			}

			function guardedRange(u, min, max) {
				if (max == min) {
					if (min == null) {
						min = 0;
						max = 100;
					}
					else {
						let delta = Math.abs(max) || 100;
						max += delta;
						min -= delta;
					}
				}

				return [min, max];
			}

			setPxRatio();

			window.addEventListener('dppxchange', setPxRatio);

			const opts = {
				title: "Scatter Plot",
				mode: 2,
				width: 1920,
				height: 600,
				legend: {
					live: false,
				},
				hooks: {
					drawClear: [
						u => {
						//	qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);

						//	qt.clear();

							// force-clear the path cache to cause drawBars() to rebuild new quadtree
							u.series.forEach((s, i) => {
								if (i > 0)
									s._paths = null;
							});
						},
					],
				},
				scales: {
					x: {
						time: false,
					//	auto: false,
					//	range: [0, 500],
						// remove any scale padding, use raw data limits
						range: guardedRange,
					},
					y: {
					//	auto: false,
					//	range: [0, 500],
						// remove any scale padding, use raw data limits
						range: guardedRange,
					},
				},
				series: [
					{
						/*
						stroke: "red",
						fill: "rgba(255,0,0,0.1)",
						paths: (u, seriesIdx, idx0, idx1) => {
							uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
								let d = u.data[seriesIdx];

								console.log(d);
							});

							return null;
						},
						*/
					},
					{
						stroke: "red",
						fill: "rgba(255,0,0,0.1)",
						paths: drawPoints,
					},
					{
						stroke: "green",
						fill: "rgba(0,255,0,0.1)",
						paths: drawPoints,
					},
					{
						stroke: "blue",
						fill: "rgba(0,0,255,0.1)",
						paths: drawPoints,
					},
					{
						stroke: "magenta",
						fill: "rgba(0,0,255,0.1)",
						paths: drawPoints,
					},
				],
			};

			let u = new uPlot(opts, data, document.body);



			points = 50;

			// size range in pixels (diameter)
			let minSize = 6;
			let maxSize = 60;

			let maxArea = Math.PI * (maxSize / 2) ** 2;
			let minArea = Math.PI * (minSize / 2) ** 2;

			// quadratic scaling (px area)
			function getSize(value, minValue, maxValue) {
				let pct = value / maxValue;
				// clamp to min area
				//let area = Math.max(maxArea * pct, minArea);
				let area = maxArea * pct;
				return Math.sqrt(area / Math.PI) * 2;
			}

			function getSizeMinMax(u) {
				let minValue = Infinity;
				let maxValue = -Infinity;

				for (let i = 1; i < u.series.length; i++) {
					let sizeData = u.data[i][2];

					for (let j = 0; j < sizeData.length; j++) {
						minValue = Math.min(minValue, sizeData[j]);
						maxValue = Math.max(maxValue, sizeData[j]);
					}
				}

				return [minValue, maxValue];
			}

			let drawPoints3 = makeDrawPoints3({
				disp: {
					size: {
						unit: 3, // raw CSS pixels
					//	discr: true,
						values: (u, seriesIdx, idx0, idx1) => {
							// TODO: only run once per setData() call
							let [minValue, maxValue] = getSizeMinMax(u);
							return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
						},
					},
				},
				each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
					// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
					lft -= u.bbox.left;
					top -= u.bbox.top;
					qt.add({x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx});
				},
			});

			let data2 = filledArr(series, v => [
				filledArr(points, i => randInt(0,500)),
				filledArr(points, i => randInt(0,500)),
				filledArr(points, i => randInt(1,10000)),  // bubble size, population
				filledArr(points, i => (Math.random() + 1).toString(36).substring(7)), // label / country name
			]);

			data2[0] = null;
			data2[1][1] = data2[1][1].map(v => v == 0 ? v : v * -1);

			/*
			const legendValue = (u, val, seriesIdx, idx) => {
				let [x, y, size] = u.data[seriesIdx];
				return `${x[idx]}, ${y[idx]}, ${size[idx]}`;
			};
			*/

			const legendValues = (u, seriesIdx, dataIdx) => {
				// when data null, it's initial schema probe (also u.status == 0)
				if (u.data == null || dataIdx == null || hRect == null || hRect.sidx != seriesIdx) {
					return {
						"Country":    '<NUM COUNTRIES>',
						"Population": '<TOTAL POP>',
						"GDP":        '<TOTAL GDP>',
						"Income":     '<AVG INCOME>'
					};
				}

				let [x, y, size, label] = u.data[seriesIdx];

				return {
					"Country":    label[hRect.didx],
					"Population": size[hRect.didx],
					"GDP":        '$' + x[hRect.didx],
					"Income":     '$' + y[hRect.didx],
				};
			};

			// hovered
			let hRect;

			const opts2 = {
				title: "Bubble Plot",
				mode: 2,
				width: 1920,
				height: 600,
				legend: {
				//	live: false,
				},
				cursor: {
					dataIdx: (u, seriesIdx) => {
						if (seriesIdx == 1) {
							hRect = null;

							let dist = Infinity;
							let area = Infinity;
							let cx = u.cursor.left * pxRatio;
							let cy = u.cursor.top * pxRatio;

							qt.get(cx, cy, 1, 1, o => {
								if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
									let ocx = o.x + o.w / 2;
									let ocy = o.y + o.h / 2;

									let dx = ocx - cx;
									let dy = ocy - cy;

									let d = Math.sqrt(dx ** 2 + dy ** 2);

									// test against radius for actual hover
									if (d <= o.w / 2) {
										let a = o.w * o.h;

										// prefer smallest
										if (a < area) {
											area = a;
											dist = d;
											hRect = o;
										}
										// only hover bbox with closest distance
										else if (a == area && d <= dist) {
											dist = d;
											hRect = o;
										}
									}
								}
							});
						}

						return hRect && seriesIdx == hRect.sidx ? hRect.didx : null;
					},
					points: {
						size: (u, seriesIdx) => {
							return hRect && seriesIdx == hRect.sidx ? hRect.w / pxRatio : 0;
						}
					},
					focus: {
						prox: 1e3,
						alpha: 0.3,
						dist: (u, seriesIdx) => {
							let prox = (hRect?.sidx === seriesIdx ? 0 : Infinity);
							return prox;
						},
					}
				},
				hooks: {
					setSeries: [ (u, seriesIdx) => console.log('setSeries', seriesIdx) ],
					setLegend: [ u => console.log('setLegend', u.legend.idxs) ],
					drawClear: [
						u => {
							qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);

							qt.clear();

							// force-clear the path cache to cause drawBars() to rebuild new quadtree
							u.series.forEach((s, i) => {
								if (i > 0)
									s._paths = null;
							});
						},
					],
				},
				axes: [
					{
						label: "GDP",
					},
					{
						label: "Income 1",
					},
					{
						side: 1,
						scale: 'y2',
						stroke: "red",
						label: "Income 2",
						grid: {
							show: false,
						},
					},
				],
				scales: {
					x: {
						time: false,
					//	auto: false,
					//	range: [0, 500],

						// remove any scale padding, use raw data limits
						range: guardedRange,
					},
					y: {
					//	auto: false,
					//	range: [0, 500],

						// remove any scale padding, use raw data limits
						range: guardedRange,
					},
					y2: {
						dir: 1,
						ori: 1,
						range: guardedRange,
					}
				},
				series: [
					null,
					{
						facets: [
							{
								scale: 'x',
								auto: true,
							},
							{
								scale: 'y2',
								auto: true,
							}
						],
						label: "Region A",
						stroke: "red",
						fill: "rgba(255,0,0,0.3)",
						paths: drawPoints3,
						values: legendValues,
					},
					{
						facets: [
							{
								scale: 'x',
								auto: true,
							},
							{
								scale: 'y',
								auto: true,
							}
						],
						label: "Region B",
						stroke: "green",
						fill: "rgba(0,255,0,0.3)",
						paths: drawPoints3,
						values: legendValues,
					},
					{
						facets: [
							{
								scale: 'x',
								auto: true,
							},
							{
								scale: 'y',
								auto: true,
							}
						],
						label: "Region C",
						stroke: "blue",
						fill: "rgba(0,0,255,0.3)",
						paths: drawPoints3,
						values: legendValues,
					},
					{
						facets: [
							{
								scale: 'x',
								auto: true,
							},
							{
								scale: 'y',
								auto: true,
							}
						],
						label: "Region E",
						stroke: "orange",
						fill: "rgba(255,128,0,0.3)",
						paths: drawPoints3,
						values: legendValues,
					},
				],
			};

			let u2 = new uPlot(opts2, data2, document.body);
		</script>
	</body>
</html>